﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

namespace MartinRL.ProjectEuler
{
    public static class Extensions
    {
        public static double Log10(this double @this)
        {
            return Math.Log10(@this);
        }

        public static bool IsPrime(this long @this)
        {
            if (@this == 2) return true;

            if (@this.IsEven()) return false;

            var maxFactor = @this.CalculateMaxFactor();
            for (var t = 3; t <= maxFactor; t = t + 2)
            {
                if (@this % t == 0) return false;
            }

            return @this != 1;
        }

        public static bool IsPrime(this int @this)
        {
            if (@this == 2) return true;

            if (@this.IsEven()) return false;

            var maxFactor = @this.CalculateMaxFactor();
            for (var t = 3; t <= maxFactor; t = t + 2)
            {
                if (@this % t == 0) return false;
            }

            return @this != 1;
        }

        public static bool IsPrime(this ushort @this)
        {
            if (@this == 2) return true;

            if (@this.IsEven()) return false;

            var maxFactor = @this.CalculateMaxFactor();
            for (var t = 3; t <= maxFactor; t = t + 2)
            {
                if (@this % t == 0) return false;
            }

            return @this != 1;
        }

        public static int SumOfProperDivisors(this int @this)
        {
            var sum = 1;

	        2.To(@this.Sqrt()).ForEach(t => 
            {
                if (@this % t == 0)
                {
                    sum += t;

                    if (t != @this / t)
                    {
                        sum += @this / t; 
                    }
                }
            });

            return sum;
        }

        public static bool IsEven(this int @this)
        {
            return @this % 2 == 0;
        }

        public static bool IsEven(this long @this)
        {
            return @this % 2 == 0;
        }

        public static bool IsEven(this ushort @this)
        {
            return @this % 2 == 0;
        }

        public static int Product(this IEnumerable<int> values)
        {
            return values.Aggregate((a, b) => a * b);
        }

        public static long CalculateMaxFactor(this long @this)
        {
            return (long)Math.Sqrt(@this);
        }

        public static int CalculateMaxFactor(this int @this)
        {
            return (int)Math.Sqrt(@this);
        }

        public static short CalculateMaxFactor(this short @this)
        {
            return (short)Math.Sqrt(@this);
        }

        public static ushort CalculateMaxFactor(this ushort @this)
        {
            return (ushort)Math.Sqrt(@this);
        }

        public static IEnumerable<int> End(this IEnumerable<int> @this, int endTerm)
        {
            return @this.TakeWhile(t => t <= endTerm);
        }

        public static bool IsEvenlyDivisibleBy(this int @this, int divisor)
        {
            if (divisor == 0)
                return false;

            return @this % divisor == 0;
        }

        public static bool IsEvenlyDivisibleBy(this long @this, long divisor)
        {
            if (divisor == 0)
                return false;

            return @this % divisor == 0;
        }

        public static bool IsEvenlyDivisibleBy(this long @this, params long[] divisors)
        {
            return divisors.All(d => @this.IsEvenlyDivisibleBy(d));
        }

        public static bool IsEvenlyDivisibleBy(this int @this, params int[] divisors)
        {
            return divisors.All(d => @this.IsEvenlyDivisibleBy(d));
        }

        public static double Pow(this double @this, double power)
        {
            return Math.Pow(@this, power);
        }

        public static int Pow(this int @this, int power)
        {
            return (int)Math.Pow(@this, power);
        }

        public static int Pow(this int @this, ushort power)
        {
            return (int)Math.Pow(@this, power);
        }

        public static int Pow(this byte @this, byte power)
        {
            return (int)Math.Pow(@this, power);
        }

        public static BigInteger BigPow(this int @this, int power)
        {
            return BigInteger.Pow(@this, power);
        }

        public static double Sqr(this double @this)
        {
            return Pow(@this, 2);
        }

        public static int Sqr(this int @this)
        {
            return (int)Sqr((double)@this);
        }

        public static ushort Sqr(this ushort @this)
        {
            return (ushort)(@this * @this);
        }

        public static int Sqrt(this int @this)
        {
            return (int)Math.Sqrt(@this);
        }

        public static double Sqrt(this double @this)
        {
            return Math.Sqrt(@this);
        }

        public static int ParseAsInt(this char @this)
        {
            return ParseAsInt(new string(@this, 1));
        }

        public static uint ParseAsUInt(this string @this)
        {
            return uint.Parse(@this);
        }

        public static ushort ParseAsUShort(this string @this)
        {
            return ushort.Parse(@this);
        }

        public static int ParseAsInt(this string @this)
        {
            return int.Parse(@this);
        }

        public static byte ParseAsByte(this char @this)
        {
            return byte.Parse(new string(new[]{@this}));
        }

        public static long ParseAsLong(this string @this)
        {
            return long.Parse(@this);
        }

        public static BigInteger ParseAsBigInteger(this string @this)
        {
            return BigInteger.Parse(@this);
        }

        public static void ForEach<T>(this IEnumerable<T> @this, Action<T> action)
        {
            foreach (var element in @this)
            {
                action(element);
            }
        }

        public static string FormatWith(this string @this, params object[] args)
        {
            return string.Format(@this, args);
        }

        /// <summary>
        /// Spec: http://www.math.mtu.edu/mathlab/COURSES/holt/dnt/divis2.html
        /// </summary>
        public static int NumberOfDivisors(this int @this)
        {
            if (@this == 1)
                return 1;

            // 2do: subtract one if @this is a «perfect square»
            return 1.To(@this.Sqrt())
                       .Count(t => @this.IsEvenlyDivisibleBy(t))
                   * 2;
        }

        public static BigInteger Sum(this IEnumerable<BigInteger> @this)
        {
            BigInteger sum = 0;

            @this.ForEach(bi => sum = sum + bi);

            return sum;
        }

        public static string Truncate(this string @this, int length)
        {
            return @this.Substring(0, length);
        }

        public static IEnumerable<string> SplitWithNewLineSeparator(this string @this)
        {
            return @this.Split(Environment.NewLine.ToCharArray()[0]);
        }

        public static IEnumerable<string> SplitWithCommaSeparator(this string @this)
        {
            return @this.Split(',');
        }

        public static IEnumerable<string> Split(this string @this, char separator)
        {
            return @this.Split(new[]{separator}, StringSplitOptions.RemoveEmptyEntries);
        }

        public static IEnumerable<int> ToMax(this int @this)
        {
            return @this.To(int.MaxValue);
        }

        public static IEnumerable<int> To(this int @this, int to)
        {
            for (var i = @this; i <= to; i++)
                yield return i;
        }

        public static IEnumerable<short> To(this short @this, short to)
        {
            for (var i = @this; i <= to; i++)
                yield return i;
        }

        public static IEnumerable<ushort> ToMax(this ushort @this)
        {
            return @this.To(ushort.MaxValue);
        }

        public static IEnumerable<ushort> To(this ushort @this, ushort to)
        {
            for (var i = @this; i <= to; i++)
                yield return i;
        }
        
        public static IEnumerable<DateTime> To(this DateTime @this, DateTime to)
        {
            for (var i = 0; i <= (to - @this).Days; i++)
                yield return @this.AddDays(i);
        }

        public static BigInteger BigFactorial(this int @this)
        {
            if (@this == 1)
                return @this;

            return @this * BigFactorial(@this - 1);
        }

        public static int Factorial(this int @this)
        {
            if (@this == 0 || @this == 1)
                return 1;

            return @this * Factorial(@this - 1);
        }

        public static uint Sum(this IEnumerable<byte> @this)
        {
            uint sum = 0;

            @this.ForEach(b => sum = sum + b);

            return sum;
        }

        public static IEnumerable<byte> ToByteSequence(this IEnumerable<char> @this)
        {
            return @this.Select(c => c.ParseAsByte());
        }

        public static string RemoveAll(this string @this, string remove)
        {
            return @this.Replace(remove, string.Empty);
        }

        /// <summary>
        /// <remarks>only parts relevant to problem 17 are included</remarks>
        /// </summary>
        public static string AsWord(this int @this)
        {
            const string HUNDRED = "hundred";
            const string ONE = "one";

            var thisAsString = @this.ToString();

            if (@this < 100 && ((NumberAsWord)@this).ToString() != thisAsString)
                return ((NumberAsWord)@this).ToString();

            if (@this > 20 && @this < 100)
                return thisAsString.ToCharArray().Reverse()
                    .Select((c, i) => c.ParseAsInt() * 10.Pow(i)).Reverse()
                    .Select(t => ((NumberAsWord) t).ToString())
                    .JoinAsString(" ");

            if (@this > 100 && @this < 1000)
            {
                var numberOfHundreds = thisAsString.ToCharArray().First().ParseAsInt();

                var thisMinusTheHundreds = @this - numberOfHundreds * 100;

                if (thisMinusTheHundreds == 0)
                    return ((NumberAsWord)numberOfHundreds) + " " + HUNDRED;

                return ((NumberAsWord) numberOfHundreds)
                       + " " + HUNDRED + " and "
                       + AsWord(thisMinusTheHundreds);
            }

            if (@this == 100)
                return ONE + " " + HUNDRED;

            if (@this == 1000)
                return ONE + " thousand";

            throw new NotSupportedException("{0} is unsupported as of now".FormatWith(thisAsString));
        }

        public static bool In<T>(this T @this, ISet<T> set)
        {
            return set.Contains(@this);
        }

        public static IEnumerable<T> Pipe<T>(this IEnumerable<T> @this, Action<T> action)
        {
            foreach (var element in @this)
            {
                action(element);

                yield return element;
            }
        }

        public static string JoinAsString(this IEnumerable<object> @this, string separator = "")
        {
            return string.Join(separator, @this);
        }

        public static byte ParseAsByte(this string @this)
        {
            return byte.Parse(@this);
        }

        public static uint ParseAsUint(this string @this)
        {
            return uint.Parse(@this);
        }

        internal enum NumberAsWord
        {
            one = 1,
            two = 2,
            three = 3,
            four = 4,
            five = 5,
            six = 6,
            seven = 7,
            eight = 8,
            nine = 9,
            ten = 10,
            eleven = 11,
            twelve = 12,
            thirteen = 13,
            fourteen = 14,
            fifteen = 15,
            sixteen = 16,
            seventeen = 17,
            eighteen = 18,
            nineteen = 19,
            twenty = 20,
            thirty = 30,
            forty = 40,
            fifty = 50,
            sixty = 60,
            seventy = 70,
            eighty = 80,
            ninety = 90
        }

        public static IEnumerable<int> Repeat(this IEnumerable<int> @this, byte repeats)
        {
            var sequence = @this;
            var repeatsLeft = repeats;

            while (repeatsLeft > 0)
            {
                sequence = sequence.Concat(sequence);

                repeatsLeft--;
            }

            return sequence;
        }
    }
}